import type { CallbackFunction, LazyOptions, ValueFormatterObject } from '../types/lazy'
import type { CssStyleObject } from '../types/util'
import { assign, hasIntersectionObserver, isObject } from './util'
import { loadImage } from './loader'

enum LifecycleEnum {
    LOADING = 'loading',
    LOADED = 'loaded',
    ERROR = 'error',
}

const DEFAULT_OBSERVER_OPTIONS = {
    rootMargin: '0px',
    threshold: 0,
}

const DEFAULT_LOADING = ''
const DEFAULT_ERROR = ''

export default class Lazy {
    lazyActive = true // 是否开启懒加载
    crossOrigin = true // 开启跨域
    options: LazyOptions = {
        loading: DEFAULT_LOADING,
        error: DEFAULT_ERROR,
        observerOptions: DEFAULT_OBSERVER_OPTIONS,
        log: true,
        ratioCalculator: (width: number, height: number) => height / width,
    }

    _images = new WeakMap()

    constructor(flag = true, options: LazyOptions, crossOrigin = true) {
        this.lazyActive = flag
        this.crossOrigin = crossOrigin
        this.config(options)
    }

    config(options = {} as LazyOptions) {
        assign(this.options, options)
        options.ratioCalculator && (this.options.ratioCalculator = options.ratioCalculator)
    }

    // mount
    mount(el: HTMLImageElement, binding: string | ValueFormatterObject, callback: CallbackFunction): void {
        const { src, loading, error } = this._valueFormatter(binding)
        el.setAttribute('lazy', LifecycleEnum.LOADING)
        el.setAttribute('src', loading || DEFAULT_LOADING)
        if (!this.lazyActive) {
            this._setImageSrc(el, src, callback, error)
        }
        else {
            if (!hasIntersectionObserver) {
                this._setImageSrc(el, src, callback, error)
                this._log(() => {
                    throw new Error('Not support IntersectionObserver!')
                })
            }
            this._initIntersectionObserver(el, src, callback, error)
        }
    }

    // resize
    resize(el: HTMLImageElement, callback: () => void) {
        const lazy = el.getAttribute('lazy')
        const src = el.getAttribute('src')
        if (lazy && lazy === LifecycleEnum.LOADED && src) {
            loadImage(src, this.crossOrigin).then((image) => {
                const { width, height } = image
                const curHeight = (el.width / width) * height
                el.height = curHeight
                const style = el.style as CssStyleObject
                style.height = `${curHeight}px`
                callback()
            })
        }
    }

    // unmount
    unmount(el: HTMLElement): void {
        const imgItem = this._realObserver(el)
        imgItem && imgItem.unobserve(el)
        this._images.delete(el)
    }

    /**
     * 设置img的src
     * @param {*} el - img
     * @param {*} src - 原图
     * @param {*} error - 错误图片
     * @param {*} callback - 完成的回调函数，通知组件刷新布局
     * @returns
     */
    _setImageSrc(el: HTMLImageElement, src: string, callback: CallbackFunction, error?: string): void {
        if (!src)
            return

        const preSrc = el.getAttribute('src')
        if (preSrc === src)
            return

        loadImage(src, this.crossOrigin)
            .then((image) => {
                // 修改容器
                const { width, height } = image
                const ratio = this.options.ratioCalculator?.(width, height) || height / width
                const lazyBox = el.parentNode!.parentNode as HTMLElement
                lazyBox.style.paddingBottom = `${ratio * 100}%`

                // 设置图片
                el.setAttribute('lazy', LifecycleEnum.LOADED)
                el.removeAttribute('src')
                el.setAttribute('src', src)

                callback(true)
            })
            .catch(() => {
                const imgItem = this._realObserver(el)
                imgItem && imgItem.disconnect()
                if (error) {
                    el.setAttribute('lazy', LifecycleEnum.ERROR)
                    el.setAttribute('src', error)
                    callback(false)
                }
                this._log(() => {
                    throw new Error(`Image failed to load!And failed src was: ${src} `)
                })
            })
    }

    _isOpenLazy(): boolean {
        return hasIntersectionObserver && this.lazyActive
    }

    /**
     * 添加img和对应的observer到weakMap中
     * 开启监听
     * 当出现在可视区域后取消监听
     * @param {*} el - img
     * @param {*} src - 图片
     * @param {*} error - 错误图片
     * @param {*} callback - 完成的回调函数，通知组件刷新布局
     */
    _initIntersectionObserver(el: HTMLImageElement, src: string, callback: CallbackFunction, error?: string): void {
        const observerOptions = this.options.observerOptions
        this._images.set(
            el,
            new IntersectionObserver((entries) => {
                Array.prototype.forEach.call(entries, (entry) => {
                    if (entry.isIntersecting) {
                        const imgItem = this._realObserver(el)
                        imgItem && imgItem.unobserve(entry.target)
                        this._setImageSrc(el, src, callback, error)
                    }
                })
            }, observerOptions),
        )

        const imgItem = this._realObserver(el)
        imgItem && imgItem.observe(el)
    }

    // 格式化参数
    _valueFormatter(value: ValueFormatterObject | string): ValueFormatterObject {
        let src = value as string
        let loading = this.options.loading
        let error = this.options.error
        if (isObject(value)) {
            src = (value as ValueFormatterObject).src
            loading = (value as ValueFormatterObject).loading || this.options.loading
            error = (value as ValueFormatterObject).error || this.options.error
        }
        return {
            src,
            loading,
            error,
        }
    }

    // 日志
    _log(callback: () => void): void {
        if (this.options.log)
            callback()
    }

    // 在map中获取对应img的observer事件
    _realObserver(el: HTMLElement): IntersectionObserver | undefined {
        return this._images.get(el)
    }
}
